Know what you're running before you run it.
chain is a supply chain risk dashboard for your projects. It surfaces lifecycle scripts that execute on npm install, CVEs from the OSV.dev database, typosquatting candidates, and missing lockfiles — before and after install, across all your projects at once.
brew install zigamedved/tap/chainOr build from source:
go install github.com/zigamedved/chain/cmd/chain@latest| Command | What it does |
|---|---|
chain |
Dashboard across all projects — ecosystem, dep counts, risk signals |
chain preview <dir> |
Deep view for one project: lifecycle scripts, CVEs, lockfile status |
chain audit |
CI check — exits 1 if any HIGH signals found |
chain init |
First-time setup wizard for scan paths |
Lifecycle scripts — packages that run arbitrary code during npm install (preinstall, install, postinstall, prepare). Detected by reading node_modules/*/package.json directly.
CVEs — batch-queried against the OSV.dev public API (no account required). Works for npm, Go modules, and PyPI. Use --offline to skip.
Lockfile signals — no package-lock.json / go.sum means installs can resolve different versions than tested.
Typosquatting — package names within edit distance 2 of the top 500 most-downloaded npm or Go packages.
CVEs — check which version you have (npm ls <package>) and whether it's in the affected range. Then either upgrade directly:
npm update <package>Or if it's a transitive dep you don't control, pin a resolution in package.json:
"overrides": { "lodash": "^4.17.21" }Lifecycle scripts — read the SCRIPT column. Most prepare scripts running husky or tsc are normal dev-workflow hooks and can be ignored. Scrutinize postinstall scripts that call external services or binaries (e.g. node ./report.js, node-gyp-build). To skip all scripts on install:
npm install --ignore-scriptsNo lockfile — commit a lockfile so installs are reproducible:
npm install # generates package-lock.json
go mod tidy # generates go.sumTyposquatting — verify the package is what you intended (npm info <package>). If it looks wrong, remove it and install the correct one.
# scan your configured project dirs
chain
# inspect a specific project before installing
chain preview ./my-app
# same but skip the CVE network call
chain preview --offline ./my-app
# CI: fail if any HIGH-severity signal found
chain audit --path ./my-app
# fail on medium+ (stricter)
chain audit --severity medium
# include lifecycle scripts in CI failure threshold
chain audit --scriptsDashboard (chain)
PROJECT ECOSYSTEM DIRECT TOTAL SCRIPTS SIGNALS
my-api node 12 847 3 ! CVE-2021-23337 ◐ no lockfile
backend go 8 86 — —
ml-pipeline python 15 — — ◐ no lockfile
DIRECT— deps you declared inpackage.json/go.mod/requirements.txtTOTAL— everything installed (from lockfile);—means no lockfile foundSCRIPTS— packages withpostinstall/install/preparehooks that run onnpm install;—for non-node or nonode_modules/SIGNALS—!is HIGH severity (CVE, committed secret),◐is MEDIUM (no lockfile, typosquat)
Audit output (chain audit)
FAIL my-api high GHSA-xxxx-yyyy-zzzz in lodash
FAIL my-api high postinstall script in esbuild
ok backend
Each FAIL line is one unique finding: project · severity · advisory ID · package. The same GHSA ID will appear once per affected package, not once per lockfile entry. Exit code 1 means at least one HIGH finding; 0 means clean.
Preview (chain preview)
Shows a summary box (dep counts, CVE count, script count) followed by two tables — lifecycle scripts that will run at install time, and the full risk signal list with package name, severity, kind, and detail.
chain audit is designed to drop into any pipeline. It exits 0 on success and 1 on findings, so it works as a standard CI gate:
GitHub Actions
- name: Dependency security check
run: chain audit --path .Fail on medium+ severity
- run: chain audit --severity medium --path .Include lifecycle script signals
- run: chain audit --scripts --path .Skip CVE network call in air-gapped environments
- run: chain audit --offline --path .chain sends a single batch request to https://api.osv.dev/v1/querybatch with every resolved package and version from your lockfile. No API key, no account, no data stored remotely. Pass --offline to skip the network call entirely.
Ecosystem mapping: package-lock.json → npm · go.sum → Go · requirements.txt → PyPI
On first run, chain init asks where your projects live and writes ~/.config/chain/config.toml. Override at any time with --path:
chain --path ~/code/my-app --path ~/code/another-appMIT
